########################################################################
#	Hello Worlds - Libre 3D RPG game.
#	Copyright (C) 2020  CYBERDEViL
#
#	This file is part of Hello Worlds.
#
#	Hello Worlds is free software: you can redistribute it and/or modify
#	it under the terms of the GNU General Public License as published by
#	the Free Software Foundation, either version 3 of the License, or
#	(at your option) any later version.
#
#	Hello Worlds is distributed in the hope that it will be useful,
#	but WITHOUT ANY WARRANTY; without even the implied warranty of
#	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#	GNU General Public License for more details.
#
#	You should have received a copy of the GNU General Public License
#	along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
########################################################################

# Panda3d
from panda3d.core import BitMask32
from panda3d.core import Vec3
from panda3d.core import WindowProperties
from panda3d.core import PandaNode, NodePath, TextNode
from panda3d.core import Camera, OrthographicLens, PGTop

from panda3d.bullet import BulletCharacterControllerNode
from panda3d.bullet import BulletCapsuleShape, ZUp

from direct.showbase import DirectObject
from direct.actor.Actor import Actor
from direct.filter.CommonFilters import CommonFilters

# Local
from core.db import Maps, AssetsPath
import os

class MiniMap(DirectObject.DirectObject):
	def __init__(self, mapId):
		self._mapId = mapId

		# Create 2d display stuff
		self.dr = base.win.makeDisplayRegion()
		self.dr.setSort(20)
		self.myCamera2d = NodePath(Camera('myCam2d'))
		lens = OrthographicLens()
		lens.setFilmSize(2, 2)
		lens.setNearFar(-1000, 1000)
		self.myCamera2d.node().setLens(lens)

		self.myRender2d = NodePath('myRender2d')
		self.myRender2d.setDepthTest(False)
		self.myRender2d.setDepthWrite(False)
		self.myCamera2d.reparentTo(self.myRender2d)
		self.dr.setCamera(self.myCamera2d)

		self._aspectRatio = base.getAspectRatio()
		self.myAspect2d = self.myRender2d.attachNewNode(PGTop('myAspect2d'))
		self.myAspect2d.setScale(1.0 / self._aspectRatio, 1.0, 1.0)

		# we now get buffer thats going to hold the texture of our new scene
		#self.mapWidth = Maps[mapId].width
		#self.mapHeight = Maps[mapId].height
		self.mapWidth = 100
		self.mapHeight = 100 # TODO

		self.mapWidthRatio = self.mapWidth / self.mapHeight
		self.mapHeightRatio = self.mapHeight / self.mapWidth
		if self.mapWidthRatio > 1: self.mapHeightRatio = 1
		else: self.mapWidthRatio = 1

		self.textureBuffer = base.win.makeTextureBuffer("miniMapBuffer", 256, 256)

		# now we have to setup a new scene graph to make this scene
		self.miniMapRender = NodePath("miniMapRender")

		# this takes care of setting up ther camera properly
		self.altCam = base.makeCamera(self.textureBuffer)
		self.altCam.reparentTo(self.miniMapRender)
		self.altCam.setPos(0, -24, 0)

		self._2dMap = loader.loadModel(
			Maps[mapId].orthoFilePath
		)
		self._2dMap.setScale(self.mapWidthRatio, 1, self.mapHeightRatio)
		self._2dMap.reparentTo(self.miniMapRender)

		self.altCam.lookAt(self._2dMap)

		self._model = loader.loadModel(os.path.join(AssetsPath.widgets, "miniMap.egg"))
		self._model.reparentTo(self.myAspect2d)
		self._model.setPos(-1.50, 0, 0.7)

		self.resize()

		projectionObj = self._model.find("**/miniMap")
		tex = self.textureBuffer.getTexture()
		projectionObj.setTexture(tex, 1)
		projectionObj.reparentTo(self._model)

		# Player head
		self._playerHead = self._model.find("**/miniMapHead")
		self._playerHead.reparentTo(self._model)

		# Zoom buttons
		self._zoomInButton = self._model.find("**/miniMapZoomIn")
		self._zoomOutButton = self._model.find("**/miniMapZoomOut")
		self._zoomInButton.reparentTo(self._model)
		self._zoomOutButton.reparentTo(self._model)

		self.accept("m", self.zoomIn)
		self.accept("n", self.zoomOut)
		self.accept("b", self.resize)

		self.accept("v", base.bufferViewer.toggleEnable)
		base.bufferViewer.setPosition("llcorner")
		base.bufferViewer.setCardSize(1.0, 0.0)

		base.mouseHandler.connect('mouse1', self._onLeftMouseClick, priority=1)
		#base.mouseHandler.connect('wheel_up', self._onLeftMouseClick, priority=1)
		#base.mouseHandler.connect('wheel_down', self._onLeftMouseClick, priority=1)
		self.accept('playerRotateEvent', self._onPlayerRotateEvent)
		self.accept('playerPositionUpdateEvent', self._onPlayerPositionUpdateEvent)
		self.accept('WINDOW_RESIZED', self.resize)

	def __del__(self):
		print("Destroyed MiniMap")

	"""Events
	"""
	def resize(self):
		""" Keep the minimap always the same size.
		"""

		# Size of the miniMap-frame as reference
		blenderUnitWidth = 0.5
		blenderUnitHeight = 0.5

		screenWidth = base.win.getProperties().getXSize()
		screenHeight = base.win.getProperties().getYSize()
		ratio = base.getAspectRatio()

		zeroXPx = screenWidth / 2
		zeroZPx = screenHeight / 2

		oneUnitPerPixelX = ratio / zeroXPx
		oneUnitPerPixelZ = 1 / zeroZPx

		wantPixelsWidth = 175
		wantPixelsHeight = 175

		currentWidthPx = blenderUnitWidth / oneUnitPerPixelX
		currentHeightPx = blenderUnitHeight / oneUnitPerPixelZ

		widthScale = wantPixelsWidth / currentWidthPx
		heightScale = wantPixelsHeight / currentHeightPx

		self._model.setScale(widthScale, 1, heightScale)

		# Now re-position (TODO re-evaluate this method)
		wantPixelsTop = 25
		wantPixelsLeft = 15

		topPx = 1 - ((wantPixelsTop + (wantPixelsHeight / 2)) * oneUnitPerPixelZ)
		leftPx = ratio - ((wantPixelsLeft + (currentWidthPx / 2)) * oneUnitPerPixelX) # move to right

		self._model.setPos(leftPx, 0, topPx)

	def destroy(self):
		self.ignoreAll()
		self.removeAllTasks()

		loader.unloadModel(self._model)
		self._model.removeNode()
		del self._model
		self._model = None

		loader.unloadModel(self._2dMap)
		del self._2dMap
		self._2dMap = None

		self.altCam.removeNode()
		self.altCam = None

		self.myCamera2d.removeNode()
		self.myCamera2d = None
		self.myRender2d.removeNode()
		self.myRender2d = None
		self.myAspect2d.removeNode()
		self.myAspect2d = None
		self.miniMapRender.removeNode()
		self.miniMapRender = None

		base.graphicsEngine.removeWindow(self.textureBuffer)
		self.textureBuffer = None

		self.dr = None

	def _onPlayerRotateEvent(self, omega):
		self.altCam.setR(self.altCam, -omega)

	def _onPlayerPositionUpdateEvent(self, characterPos):
		self.altCam.setX(((characterPos[0] / self.mapWidth) * 16) * self.mapWidthRatio) # 2d map == 16x16 blender units
		self.altCam.setZ(((characterPos[1] / self.mapHeight) * 16) * self.mapHeightRatio)

	def _onLeftMouseClick(self, args=[]):
		mpos = base.mouseWatcherNode.getMouse()
		mousePos = (mpos.getX() * self._aspectRatio, mpos.getY())

		zoomInButtonPos = (
			((self._model.getX() + self._zoomInButton.getX())),
			self._model.getZ() + self._zoomInButton.getZ()
		);

		zoomOutButtonPos = (
			((self._model.getX() + self._zoomOutButton.getX())),
			self._model.getZ() + self._zoomOutButton.getZ()
		);

		margin = 0.03

		if (mousePos[0] + margin >=  zoomInButtonPos[0]
		and mousePos[0] - margin <=  zoomInButtonPos[0]
		and mousePos[1] + margin >=  zoomInButtonPos[1]
		and mousePos[1] - margin <=  zoomInButtonPos[1]):
			self.zoomIn()
			return True

		elif (mousePos[0] + margin >=  zoomOutButtonPos[0]
		and mousePos[0] - margin <=  zoomOutButtonPos[0]
		and mousePos[1] + margin >=  zoomOutButtonPos[1]
		and mousePos[1] - margin <=  zoomOutButtonPos[1]):
			self.zoomOut()
			return True

		else: return False

	@property
	def camera(self): return self.altCam

	# TODO it makes different on how large a map is (-4 * mapRatio?)
	def zoomIn(self):
		if self.altCam.getY() < (-24):
			self.altCam.setY(self.altCam, 1)
			return True
		return False

	# TODO it makes different on how large a map is (-96 * mapRatio?)
	def zoomOut(self):
		if self.altCam.getY() > (-128):
			self.altCam.setY(self.altCam, -1)
			return True
		return False


class PlayerProto:
	""" Bones of player (only what is necesary so client and server can
	subclass from this)
	"""
	def __init__(self, world, worldNP, character, mapId):
		self._world = world
		self._worldNP = worldNP

		self._character = character

		self._mapId = mapId

		self.crouching = False
		self.isMoving = False

		self._previousCharacterPos = Vec3()

		self.keyMap = {
			"shuffleLeft": 0,
			"shuffleRight": 0,
			"forward": 0,
			"backward": 0,
			"jump": 0
		}

	@property
	def character(self): return self._character

	def id(self): return self._id
	def setKey(self, key, value):
		self.keyMap[key] = value
		return True # to base.mouseHandler that we accepted

	def destroy(self):
		self.characterNP.removeNode()
		self._world.remove(self.characterCont) # (BulletWorld.remove())
		del self.characterNP
		del self.characterCont
		self.characterCont = None
		self.characterNP = None

	def setup(self):
		h = 0.6
		w = 0.3

		# BulletCapsuleShape(float radius, float height, BulletUpAxis up)
		shape = BulletCapsuleShape(w, h, ZUp)

		# BulletCharacterControllerNode(BulletShape shape, float step_height, str name)
		self.characterCont = BulletCharacterControllerNode(shape, 0.5, 'Player')
		self.characterCont.setGravity(18.0)
		self.characterCont.setMaxSlope(0.5)
		self.characterNP = self._worldNP.attachNewNode(self.characterCont)
		self.characterNP.setPos(*self.character.spawnData.pos)
		self.characterNP.setH(self.character.spawnData.orientation)
		self.characterNP.setCollideMask(BitMask32.allOn())
		self._world.attach(self.characterCont)

	def rotate(self, omega):
		self.characterNP.setH(self.characterNP, omega)

	def rotateLeft(self, dt):
		omega = 120 * dt
		self.characterNP.setH(self.characterNP, omega)
		return omega

	def rotateRight(self, dt):
		omega = -120 * dt
		self.characterNP.setH(self.characterNP, omega)
		return omega

	def forward(self, dt, moveVec):
		moveVec.setY(6 * dt)

	def backward(self, dt, moveVec):
		moveVec.setY(-3 * dt)

	def shuffleLeft(self, dt, moveVec):
		moveVec.setX(-3 * dt)

	def shuffleRight(self, dt, moveVec):
		moveVec.setX(3 * dt)

	def setPos(self, moveVec):
		self.characterNP.setPos(self.characterNP, moveVec)

	def setGlobalPos(self, moveVec):
		self.characterNP.setPos(moveVec)

	def setOrientation(self, o):
		self.characterNP.setH(o)

	def setGlobalX(self, x): self.characterNP.setX(x)
	def setGlobalY(self, y): self.characterNP.setY(y)
	def setGlobalZ(self, z): self.characterNP.setZ(z)

	def getGlobalPos(self): return self.characterNP.getPos()
	def getOrientation(self): return self.characterNP.getH()

	def updatePreviousPos(self): # TODO find better name for this
		characterPos = self.characterNP.getPos()

		if characterPos[2] < -10:
			# reset pos (character is fallen of the map)
			self.characterNP.setPos(*self._pos) # TODO create function for this, it doesnt belong here

		if characterPos != self._previousCharacterPos:
			self._previousCharacterPos = characterPos
			self.isMoving = True
		else: self.isMoving = False

	def doJump(self):
		if self.characterCont.isOnGround() and self.isMoving:
			self.characterCont.setMaxJumpHeight(1.25)
			self.characterCont.setJumpSpeed(5.6)
			self.characterCont.setFallSpeed(16)
			self.characterCont.doJump()
		self.setKey('jump', False)

	def doCrouch(self):
		self.crouching = not self.crouching
		#sz = self.crouching and 1.2 or 1.0
		# eh this does not work
		# https://www.panda3d.org/manual/?title=Bullet_Character_Controller#Crouching
		#self.characterNP.setScale(Vec3(1, 1, sz))
		#self.characterCont.getShape().setLocalScale(Vec3(1, 1, sz))

class InputLocker(DirectObject.DirectObject):
	def __init__(self):
		self._lockInput = False
		self.accept('lockInput', self._setLockInput)

	def _setLockInput(self, state):
		self._lockInput = state

	def locked(self): return self._lockInput

class Player(PlayerProto):
	def __init__(self, world, worldNP, character, mapId):
		PlayerProto.__init__(self, world, worldNP, character, mapId)

		self.keyMap.update({
			"walk": 0,
			"rotateLeft": 0,
			"rotateRight": 0,
			"cameraUp": 0,
			"cameraDown" : 0,

			"leftMouse" : 0,
			"rightMouse" : 0
		})
		self._previousKeyMap = self.keyMap.copy()

		base.accept('space', self.setKey, ["jump", True])
		base.accept('c', self.doCrouch)

		base.accept("a", self.setKey, ["shuffleLeft", True])
		base.accept("d", self.setKey, ["shuffleRight", True])
		base.accept("w", self.setKey, ["forward", True])
		base.accept("W", self.setKey, ["walk", True])
		base.accept("shift", self.setKey, ["walk", True])
		base.accept("s", self.setKey, ["backward", True])
		base.accept("q", self.setKey, ["rotateLeft", True])
		base.accept("e", self.setKey, ["rotateRight", True])
		base.accept("r", self.setKey, ["cameraUp", True])
		base.accept("f", self.setKey, ["cameraDown", True])

		base.accept("a-up", self.setKey, ["shuffleLeft", False])
		base.accept("d-up", self.setKey, ["shuffleRight", False])
		base.accept("w-up", self.setKey, ["forward", False])
		base.accept("W-up", self.setKey, ["walk", False])
		base.accept("shift-up", self.setKey, ["walk", False])
		base.accept("s-up", self.setKey, ["backward", False])
		base.accept("q-up", self.setKey, ["rotateLeft", False])
		base.accept("e-up", self.setKey, ["rotateRight", False])
		base.accept("r-up", self.setKey, ["cameraUp", False])
		base.accept("f-up", self.setKey, ["cameraDown", False])

		self._mouseEvents = {
			"leftJustClicked" : True,
			"rightJustClicked" : True
		}
		self._cursorBackup = [0, 0]
		self._cursorPrevPos = [0, 0]
		self._currentCameraAngle = [0, 0]
		self._cursorBusy = False

		self._cameraZoomLevel = 2.0
		self._cameraZoomMax = 20
		self._cameraZoomMin = 0
		self._cameraHorizontalAngle = 0;
		self._cameraVerticalAngle = 0;


		base.mouseHandler.connect('mouse1', self.setKey, args=["leftMouse", True], priority=10)
		base.mouseHandler.connect('mouse1-up', self.setKey, args=["leftMouse", False], priority=10)

		base.mouseHandler.connect('mouse3', self.setKey, args=["rightMouse", True], priority=10)
		base.mouseHandler.connect('mouse3-up', self.setKey, args=["rightMouse", False], priority=10)

		base.mouseHandler.connect('wheel_up', self.mouseWheelUp, priority=10)
		base.mouseHandler.connect('wheel_down', self.mouseWheelDown, priority=10)

		self._cursorBackup = [0, 0]
		self._cursorPrevPos = [0, 0]
		self._animationSet = False

		self._cameraZoomLevel = 2.0
		self._cameraZoomMax = 20
		self._cameraZoomMin = 0
		self._cameraHorizontalAngle = 0;
		self._cameraVerticalAngle = 0;

		self.setup()

		self._inputLocker = InputLocker()
		self._inputChanged = False
		self._orientationChanged = False

		# Screen filter
		filters = CommonFilters(base.win, base.cam)
		filters.setBloom()

		# Task
		taskMgr.add(self.update, 'userInteract')

	@property
	def cursorBusy(self): return self._cursorBusy

	def rotate(self, omega): # override
		self._orientationChanged = True
		PlayerProto.rotate(self, omega)

	def rotateLeft(self, dt): # override
		self._orientationChanged = True
		return PlayerProto.rotateLeft(self, dt)

	def rotateRight(self, dt): # override
		self._orientationChanged = True
		return PlayerProto.rotateRight(self, dt)

	def mouseWheelUp(self):
		if (self.camFloater.getY()+0.3) < -1.0:
			self.camFloater.setY(self.camFloater.getY()+0.3)
			return True
		return False

	def mouseWheelDown(self):
		if (self.camFloater.getY()+-0.3) > -15:
			self.camFloater.setY(self.camFloater.getY()-0.3)
			return True
		return False

	def setKey(self, key, value):# override
		self._inputChanged = True
		self.keyMap[key] = value

	def moveCameraVertical(self, amount):
		newZ = self.camFloater.getZ(self.characterNP) + amount
		if newZ < 15 and newZ > -15: # Max zoomout
			self.camFloater.setZ(self.camFloater, amount)

	def processInput(self, dt):
		omega = 0.0
		mouseForward = False

		camSen = 220
		charSen = 32

		if self.keyMap["leftMouse"] or self.keyMap["rightMouse"]:
			if self.keyMap["leftMouse"] and self.keyMap["rightMouse"]:
				mouseForward = True

			if self.keyMap["rightMouse"]:
				if self._mouseEvents["rightJustClicked"]:
					self._mouseEvents["rightJustClicked"] = False
					if not self._cursorBackup[0]:
						self._cursorBusy = True
						self.showCursor(False);
						self.backupCursorPosition();
						self.centerCursor();
				else:
					# Move camera
					mw = base.mouseWatcherNode
					xDiff = mw.getMouseX()
					yDiff = mw.getMouseY()

					#camY = self.camFloater.getY() + -(yDiff * 300) * dt

					self.moveCameraVertical(-(yDiff * camSen) * dt)

					# Rotate characterNP
					omega = -xDiff * charSen
					#omega = Decimal(-xDiff * charSen).quantize(Decimal('.0001'), rounding=ROUND_DOWN)
					self.rotate(omega)
					messenger.send('playerRotateEvent', [omega])
					self.centerCursor();

			elif self.keyMap["leftMouse"]:
				if self._mouseEvents["leftJustClicked"]:
					self._mouseEvents["leftJustClicked"] = False
					if not self._cursorBackup[0]:
						self._cursorBusy = True
						self.showCursor(False);
						self.backupCursorPosition();
						self.centerCursor();
				else:
					mw = base.mouseWatcherNode
					xDiff = mw.getMouseX() * 560
					yDiff = mw.getMouseY() * 300

					self.moveCameraVertical(-yDiff * dt)

					self.floater.setH(self.floater, -xDiff * dt)
					self.centerCursor();

		elif self._cursorBackup[0]:
			self._mouseEvents["leftJustClicked"] = True
			self._mouseEvents["rightJustClicked"] = True
			self.restoreCursorPosition();
			self._cursorBusy = False
			self.showCursor();
			mouseForward = False

		# Restore camera behind character if moving and not leftmouse
		if self.keyMap['forward'] and not self.keyMap["leftMouse"]:
			if self.floater.getH() > 0.4:
				self.floater.setH(self.floater, -0.45)
			elif self.floater.getH() < -0.4:
				self.floater.setH(self.floater, 0.45)

		moveVec = Vec3()
		if self.keyMap['forward'] or mouseForward:
			self.forward(dt, moveVec)
		elif self.keyMap['backward']:
			self.backward(dt, moveVec)

		if self.keyMap['shuffleLeft']:
			self.shuffleLeft(dt, moveVec)
		elif self.keyMap['shuffleRight']:
			self.shuffleRight(dt, moveVec)

		if self.keyMap['rotateLeft']:
			messenger.send('playerRotateEvent', [self.rotateLeft(dt)])
		elif self.keyMap['rotateRight']:
			messenger.send('playerRotateEvent', [self.rotateRight(dt)])

		if self._orientationChanged:
			self._orientationChanged = False
			# Send for mini-map
			messenger.send('playerOrientationChanged', [self])

		if self.keyMap["jump"]: self.doJump()

		# Move
		self.setPos(moveVec)

		self.updatePreviousPos()
		if self.isMoving:
			messenger.send('playerPositionUpdateEvent', [self._previousCharacterPos]) # self._previousCharacterPos is the current pos, updatePreviousPos set it.

		if self.keyMap['cameraUp']:
			self.moveCameraVertical(0.2)
		elif self.keyMap['cameraDown']:
			self.moveCameraVertical(-0.2)


		if not self._animationSet and self.isMoving:
			if self.keyMap["walk"]:
				self.actorNP.loop("walk")
			else:
				self.actorNP.loop("run")
			self._animationSet = True
		elif self._animationSet and not self.isMoving:
			self.actorNP.stop()
			self.actorNP.loop("idle")
			self._animationSet = False

		base.camera.lookAt(self.floater)

	def destroy(self):
		taskMgr.remove('userInteract')
		PlayerProto.destroy(self)
		self.actorNP.cleanup()
		self.actorNP.removeNode()
		self.floater.removeNode()
		self.camFloater.removeNode()
		self.characterNameNP.removeNode()

		self.miniMap.destroy()
		self.miniMap = None

	def setup(self):
		# Create character
		PlayerProto.setup(self)

		# Character model
		self.actorNP = Actor(
			os.path.join(AssetsPath.players, self.character.file)
		)
		#self.actorNP = Actor(
		#	"assets/characters/{0}".format(self.character.model.file),
		#	self.character.model.actions)
		self.actorNP.setPlayRate(1.1, 'run')
		self.actorNP.reparentTo(self.characterNP)
		self.actorNP.setScale(0.3048) # 1ft = 0.3048m
		self.actorNP.setH(180)

		# Model to collision mesh offset TODO make dynamic
		self.actorNP.setPos(0, 0, -0.55)

		# Character camera
		self.floater = NodePath(PandaNode("floater"))
		self.floater.reparentTo(self.characterNP)
		self.floater.setZ(0.45) # Set floater height TODO make dynamic
		self.floater.setY(1.0)

		base.camera.reparentTo(self.floater)

		self.camFloater = NodePath(PandaNode("camFloater"))
		self.camFloater.setPos(0, -3, 1)

		self.camFloater.reparentTo(self.floater)
		base.camera.reparentTo(self.camFloater)

		# Player name node
		# TODO more dynamic height
		self.characterName = TextNode('characterName')
		self.characterName.setText(self.character.name)
		self.characterNameNP = self.floater.attachNewNode(self.characterName)
		self.characterNameNP.setScale(0.2)
		self.characterNameNP.setPos((0.1, -1, 0.3))

		# MiniMap
		self.miniMap = MiniMap(self._mapId)

		base.camera.lookAt( 0, 0, 0 )

	def centerCursor(self):
		props = base.win.getProperties();
		base.win.movePointer(0, int(props.getXSize() / 2), int(props.getYSize() / 2))

	def backupCursorPosition(self):
		mw = base.mouseWatcherNode
		self._cursorBackup = [mw.getMouseX(), mw.getMouseY()]

	def restoreCursorPosition(self):
		props = base.win.getProperties()
		x = int((props.getXSize() / 2) * (self._cursorBackup[0] + 1 ))
		y = int((props.getYSize() / 2) * (-self._cursorBackup[1] + 1 ))
		base.win.movePointer(0, x, y)
		self._cursorBackup = [0, 0]

	def showCursor(self, state=True):
		wprops = WindowProperties()
		wprops.setCursorHidden(not state)
		base.win.requestProperties(wprops)

	def update(self, task):
		dt = globalClock.getDt()
		self.processInput(dt)
		return task.cont
